package com.googlecode.alfrescocachingserver;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Date;
import java.util.Enumeration;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.core.HttpHeaders;

import org.apache.commons.configuration.Configuration;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.HttpRequestHandler;

import com.googlecode.alfrescocachingserver.exceptions.ServiceException;
import com.googlecode.alfrescocachingserver.model.CacheEntry;
import com.googlecode.alfrescocachingserver.model.WebScriptCacheKey;
import com.googlecode.alfrescocachingserver.model.WebScriptResponse;
import com.googlecode.alfrescocachingserver.model.WebScriptUri;
import com.googlecode.alfrescocachingserver.services.CacheService;
import com.googlecode.alfrescocachingserver.services.WebScriptConnectorService;
import com.googlecode.alfrescocachingserver.util.GlobalConstants;
import com.googlecode.alfrescocachingserver.util.WebHelper;

public class CacheRequestHandler implements HttpRequestHandler
{
    private static final Logger LOG = LoggerFactory.getLogger(CacheRequestHandler.class);
    
    private static final int CHUNK_SIZE = 1024 * 1024;  // 1MB
    
    protected CacheService cacheService;
    protected Configuration configuration;
    protected WebScriptConnectorService webScriptConnectorService;
    
    public void setCacheService(CacheService cacheService)
    {
        this.cacheService = cacheService;
    }

    public void setConfiguration(Configuration configuration)
    {
        this.configuration = configuration;
    }

    public void setWebScriptConnectorService(WebScriptConnectorService webScriptConnectorService)
    {
        this.webScriptConnectorService = webScriptConnectorService;
    }
    
    public void handleRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
    {
        long startTime = System.currentTimeMillis();
        
        LOG.debug("requestUri = {}", request.getRequestURI());
        logRequestHeaders(request);
        
        request.getHeaderNames();
        
        WebScriptUri webScriptUri = new WebScriptUri(request);
        
        WebScriptCacheKey cacheKey = new WebScriptCacheKey(
                webScriptUri,
                request.getContentType());
        
        CacheEntry cacheEntry = cacheService.getCacheEntry(cacheKey);
        
        boolean completedFlag = false;
        
        if (cacheEntry != null)
        {
            if (cacheEntry.getLocalFilePath() == null)
            {
//                IOUtils.write(cacheEntry.getValue(), response.getOutputStream());
//                completedFlag = true;
                
                completedFlag = outputResponse(request, response, cacheEntry.getValue(), cacheEntry.getHeaderMap());
            }
            else
            {
                String basePath = configuration.getString("webScriptLocalStoragePath");
                
                StringBuilder buf = new StringBuilder(basePath);
                buf.append(GlobalConstants.FILE_SEPARATOR);
                buf.append(cacheEntry.getLocalFilePath());
                
                LOG.debug("Retrieving local cache file:  {}", buf.toString());

                File file = new File(buf.toString());
                
                if (file.exists())
                {
//                    FileUtils.copyFile(file, response.getOutputStream());
//                    completedFlag = true;
                    completedFlag = outputResponse(request, response, file, cacheEntry.getHeaderMap());
                }
                else
                {
                    // Invalid cache entry, remove from cache
                    cacheService.removeCacheEntry(cacheKey);
                }
            }
        }
        
//        // TODO:  determine if uri is cacheable
//        boolean uriIsCachable = true;
//        
//        if (uriIsCachable)
//        {
//            // TODO:  Retrieve and/or create cache instance
//            Ehcache cache = CACHE_MANAGER.getEhcache("org/springframework/extensions/webscripts/indexcache.get");
//            
//            Element element = cache.get(completeUri);
//            
//            LOG.debug("element = {}", element);
//            
//            byte[] responseBytes = null;
//            
//            if (element != null)
//            {
//                MimeTypeByteArray mimeTypeByteArray = (MimeTypeByteArray)element.getObjectValue();
//                
//                responseBytes = mimeTypeByteArray.getValue();
//            }
//            else
//            {
//                // TODO:  move logic to self populating cache
//                String response = webScriptConnectorService.processRequest(
//                        completeUri,
//                        servletRequest.getMethod(),
//                        servletRequest.getContentType(),
//                        null);
//                
//                // TODO:  convert to byte[] in the webScriptConnectorService
//                responseBytes = org.apache.commons.codec.binary.StringUtils.getBytesUtf8(response);
//                
//                MimeTypeByteArray mimeTypeByteArray = new MimeTypeByteArray(servletRequest.getContentType(),
//                        org.apache.commons.codec.binary.StringUtils.getBytesUtf8(response));
//                
//                net.sf.ehcache.Element newElement = new net.sf.ehcache.Element(completeUri, mimeTypeByteArray);
//                
//                cache.put(newElement);
//            }
//
//            IOUtils.write(responseBytes, servletResponse.getOutputStream());
//        }
        
        if (!completedFlag)
        {
            WebScriptResponse webScriptResponse = webScriptConnectorService.processRequest(
                    webScriptUri,
                    request.getContentType(),
                    null);
            
//            response.getOutputStream().write(webScriptResponse.getResponseBytes());

            outputResponse(request, response, webScriptResponse.getResponseBytes(), webScriptResponse.getHeaderMap());
        }
        
        LOG.debug("processing time = {}ms", System.currentTimeMillis() - startTime);
    }
    
    private boolean outputResponse(HttpServletRequest request, HttpServletResponse response, Object input, Map<String, List<Object>> headerMap)
        throws IOException
    {
        long inputLength = 0;
        
        InputStream inputStream = null;
        
        try
        {
            if (input instanceof byte[])
            {
                byte[] bytes = (byte[])input;
                inputLength = bytes.length;
              
                inputStream = new ByteArrayInputStream(bytes);
            }
            else if (input instanceof File)
            {
                File file = (File)input;
                
                if (!file.exists())
                {
                    return false;
                }
                
                inputLength = file.length();
    
                inputStream = new FileInputStream(file);
            }
            else
            {
                throw new ServiceException("Unhandled input type when generating response:  " + input.getClass().getName());
            }

            if (headerMap.containsKey("Content-Range"))
            {
                response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
            }

            String range = request.getHeader("range");
            
            if (StringUtils.isNotBlank(range))
            {
                LOG.debug("range = {}", range);
                String[] ranges = range.split("=")[1].split("-");
                final int from = Integer.parseInt(ranges[0]);
                
                // Chunk media if the range upper bound is unspecified. Chrome sends "bytes=0-"
                int to = CHUNK_SIZE + from;
                
                if (ranges.length == 2)
                {
                    to = Integer.parseInt(ranges[1]);
                }
    
                if (to >= inputLength)
                {
                    to = (int) (inputLength - 1);
                }
                
                final int length = to - from + 1;
                
                final String responseRange = String.format("bytes %d-%d/%d", from, to, inputLength);
                
                WebHelper.updateHeaderMap(headerMap, "Accept-Ranges", "bytes", true);
                WebHelper.updateHeaderMap(headerMap, "Content-Range", responseRange, true);
                WebHelper.updateHeaderMap(headerMap, HttpHeaders.CONTENT_LENGTH, new Long(length), true);
                
                setResponseHeaders(response, headerMap);

//                response.setDateHeader(HttpHeaders.LAST_MODIFIED, inputLastModified.getTime());
                
                long writeLength = IOUtils.copyLarge(inputStream, response.getOutputStream(), from, length);
                
                LOG.debug("inputLength = {}, length = {}, writeLength = {}", inputLength, length, writeLength);
            }
            else
            {
                setResponseHeaders(response, headerMap);
                
                IOUtils.copyLarge(inputStream, response.getOutputStream());
            }
        }
        finally
        {
            IOUtils.closeQuietly(inputStream);
        }
        
        return true;
    }
    
    private void logRequestHeaders(HttpServletRequest request)
    {
        @SuppressWarnings("unchecked")
        Enumeration<String> headerNames = request.getHeaderNames();
        
        if (headerNames != null)
        {
            while (headerNames.hasMoreElements())
            {
                String headerName = headerNames.nextElement();
                
                if (StringUtils.isNotBlank(headerName))
                {
                    @SuppressWarnings("unchecked")
                    Enumeration<String> headerValues = request.getHeaders(headerName);
                    
                    while (headerValues.hasMoreElements())
                    {
                        LOG.debug("Header:  name = {}, value = {}", headerName, headerValues.nextElement());
                    }
                }
            }
        }
    }
    
    private void setResponseHeaders(HttpServletResponse response, Map<String, List<Object>> headerMap)
    {
        for (Entry<String, List<Object>> entry : headerMap.entrySet())
        {
            String headerName = entry.getKey();
            
            for (Object obj : entry.getValue())
            {
                if (obj instanceof String)
                {
                    String value = (String)obj;
                    
                    response.setHeader(headerName, value);
                    LOG.debug("Set response header:  name = {}, value = {}", headerName, value);
                }
                else if (obj instanceof Date)
                {
                    Date value = (Date)obj;
                    
                    response.setDateHeader(headerName, value.getTime());
                    LOG.debug("Set response header:  name = {}, value = {}", headerName, value);
                }
                else if (obj instanceof Integer)
                {
                    Integer value = (Integer)obj;
                    
                    response.setIntHeader(headerName, value);
                    LOG.debug("Set response header:  name = {}, value = {}", headerName, value);
                }
                else if (obj instanceof Long)
                {
                    Long value = (Long)obj;
                    
                    response.setIntHeader(headerName, value.intValue());
                    LOG.debug("Set response header:  name = {}, value = {}", headerName, value);
                }
                else
                {
                    throw new ServiceException("Unhandled header class when setting resposne headers:  " + obj.getClass().getName());
                }
            }
        }
    }
}
